﻿using System;
using System.Collections.Generic;
using System.Linq;
using Nop.Core;
using Nop.Core.Domain.Logistics;
using Nop.Core.Domain.Shipping;
using Nop.Services.Logging;
using Nop.Services.Orders;
using Nop.Services.Security;

namespace Nop.Services.Logistics
{
    /// <summary>
    /// Logistics processing service
    /// </summary>
    public class LogisticsProcessingService : ILogisticsProcessingService
    {
        #region Fields

        private readonly ILogisticsService _logisticsService;
        private readonly Nop.Services.Orders.IOrderProcessingService _orderProcessingService;
        private readonly IOrderService _orderService;
        private readonly IPermissionService _permissionService;
        private readonly LogisticsInformationSettings _logisticsInformationSettings;
        private readonly IWorkContext _workContext;
        private readonly ILogger _logger;

        #endregion

        #region Ctor

        public LogisticsProcessingService(
            ILogisticsService logisticsService, IPermissionService permissionService,
            Nop.Services.Orders.IOrderProcessingService orderProcessingService,
            IOrderService orderService,
            LogisticsInformationSettings logisticsInformationSettings,
            IWorkContext workContext, ILogger logger)
        {
            this._logisticsService = logisticsService;
            this._orderProcessingService = orderProcessingService;
            this._orderService = orderService;
            this._permissionService = permissionService;
            this._logisticsInformationSettings = logisticsInformationSettings;
            this._workContext = workContext;
            this._logger = logger;
        }

        #endregion

        #region Utilities

        protected void DumpPackageState(string message, IPackageHandle handle)
        {
            if (_logisticsInformationSettings.EnableDumpState)
            {
                _logger.Debug(String.Format("{0}: {1}", message, handle.GetStateString()));
            }
        }

        #endregion

        #region Methods

        public IPackageHandle CreatePackageHandle(LogisticsPackage package)
        {
            if (package == null)
                throw new ArgumentException("package");

            bool canManage =
                _permissionService.Authorize(StandardPermissionProvider.ManageLogistics, _workContext.CurrentCustomer) ||
                _permissionService.Authorize(StandardPermissionProvider.ManageWarehouse, _workContext.CurrentCustomer);

            var boundOrder = package.Shipment.Order.BoundOrderId.HasValue ?
                               _orderService.GetOrderById(package.Shipment.Order.BoundOrderId.Value) : null;

            var boundShipment = (boundOrder != null) ?
                boundOrder.Shipments.SingleOrDefault(x => x.TrackingNumber.Equals(package.Shipment.TrackingNumber, StringComparison.Ordinal)) : null;

            return PackageState.CreatePackageState(
                package, 
                _logisticsService,
                _orderProcessingService,
                boundShipment,
                _logisticsInformationSettings, 
                _workContext.CurrentCustomer.Id,
                canManage);
        }

        #region Cancel

        public bool CanCancelPackage(LogisticsPackage package)
        {
            return CreatePackageHandle(package).CanCancel();
        }

        public bool CancelPackage(LogisticsPackage package)
        {
            var handle = CreatePackageHandle(package);
            if (!handle.CanCancel())
            {
                DumpPackageState("CancelPackage", handle);
                return false;
            }

            handle.Cancel();
            return true;
        }

        #endregion

        #region Change address

        public virtual bool CanChangePickupAddress(LogisticsPackage package)
        {
            return CreatePackageHandle(package).CanChangePickupAddress();
        }

        public virtual bool ChangePackagePickupAddress(LogisticsPackage package, int addressId)
        {
            var handle = CreatePackageHandle(package);
            if (!handle.CanChangePickupAddress())
            {
                DumpPackageState("ChangePickupAddress", handle);
                return false;
            }

            handle.ChangePickupAddress(addressId);
            return true;
        }

        public virtual bool CanChangeDeliveryAddress(LogisticsPackage package)
        {
            bool isFree;
            return CanChangeDeliveryAddress(package, out isFree);
        }

        public virtual bool CanChangeDeliveryAddress(LogisticsPackage package, out bool isFree)
        {
            var handle = CreatePackageHandle(package);
            isFree = handle.IsChangeDeliveryAddressFree();
            return handle.CanChangeDeliveryAddress();
        }

        public virtual bool ChangePackageDeliveryAddress(LogisticsPackage package, int addressId)
        {
            var handle = CreatePackageHandle(package);
            if (!handle.CanChangeDeliveryAddress())
            {
                DumpPackageState("ChangeDeliveryAddress", handle);
                return false;
            }

            handle.ChangeDeliveryAddress(addressId);
            return true;
        }

        #endregion

        #region Update

        public virtual List<ShippingStatus> GetNextStatuses(LogisticsPackage package)
        {
            return CreatePackageHandle(package).GetNextStatuses();
        }

        public virtual bool UpdateStatus(LogisticsPackage package, ShippingStatus status)
        {
            var handle = CreatePackageHandle(package);
            if (!handle.GetNextStatuses().Contains(status))
            {
                DumpPackageState("UpdateStatus->" + status.ToString(), handle);
                return false;
            }

            handle.UpdateStatus(status);
            return true;
        }

        #endregion

        #region Pickup

        public virtual void RevertCollected(LogisticsPackage package)
        {
            if (package == null)
                throw new ArgumentNullException("package");

            if (package.Status != ShippingStatus.Delivering && package.Status != ShippingStatus.ReturningWarehouse
                    && package.Status != ShippingStatus.FailedPickUp)
                return;

            // Update order & shipment
            if (package.Shipment != null && package.Shipment.Order != null)
            {
                _orderProcessingService.UndoShip(package.Shipment);

                package.Status = ShippingStatus.ReadyForPickUp;
                _logisticsService.UpdatePackage(package, false);

                // hide the last event
                var collectedNote = _logisticsService.GetPackageNotesByPackageId(package.Id)
                     .First(x => x.ShippingStatusId == (int)ShippingStatus.Delivering || x.ShippingStatusId == (int)ShippingStatus.ReturningWarehouse 
                         || x.ShippingStatusId == (int)ShippingStatus.FailedPickUp);
                collectedNote.DisplayToCustomer = false;
                _logisticsService.UpdatePackageNote(collectedNote);

                // insert revertion note
                var revertNote = new PackageNote()
                {
                    PackageId = package.Id,
                    Type = PackageNoteType.SystemEvent,
                    Note = _logisticsService.GetPackageNoteContentByTemplate(PackageNoteStandardTemplates.RevertCollected, package.Shipment.Order.CustomerLanguageId),
                    CustomerId = _workContext.CurrentCustomer.Id,
                    AddressId = null,
                    DisplayToCustomer = false,
                    ShippingStatus = ShippingStatus.ReadyForPickUp,
                    CreatedOnUtc = DateTime.UtcNow
                };

                _logisticsService.CreatePackageNote(revertNote);
            }
            else
            {
                _logger.Debug("The status is not reverted due to shipment or order is missing. Package# " + package.Id.ToString());
            }
        }

        #endregion

        #region Delivery

        public virtual bool FailDelivery(LogisticsPackage package, PackageNoteTemplate failedReasonTemplate)
        {
            var handle = CreatePackageHandle(package);
            if (!handle.CanDeliver())
            {
                DumpPackageState("FailDelivery", handle);
                return false;
            }
            
            handle.FailDelivery(failedReasonTemplate ?? PackageNoteStandardTemplates.GeneralFailedDelivery);
            return true;
        }

        public virtual void RevertDelivered(LogisticsPackage package)
        {
            if (package == null)
                throw new ArgumentNullException("package");

            if (package.Status != ShippingStatus.Delivered && package.Status != ShippingStatus.ReturningWarehouseFailedDelivery)
                return;

            // Update order & shipment
            if (package.Shipment != null && package.Shipment.Order != null)
            {
                _orderProcessingService.UndoDeliver(package.Shipment);

                package.Status = ShippingStatus.Delivering;
                _logisticsService.UpdatePackage(package, false);

                // hide the last delivered event
                var deliveredNote = _logisticsService.GetPackageNotesByPackageId(package.Id).First(x => x.ShippingStatusId == (int)ShippingStatus.Delivered 
                                                        || x.ShippingStatusId == (int)ShippingStatus.ReturningWarehouseFailedDelivery);
                deliveredNote.DisplayToCustomer = false;
                _logisticsService.UpdatePackageNote(deliveredNote);

                // insert revertion note
                var revertNote = new PackageNote()
                {
                    PackageId = package.Id,
                    Type = PackageNoteType.SystemEvent,
                    Note = _logisticsService.GetPackageNoteContentByTemplate(PackageNoteStandardTemplates.RevertDelivered, package.Shipment.Order.CustomerLanguageId),
                    CustomerId = _workContext.CurrentCustomer.Id,
                    AddressId = null,
                    DisplayToCustomer = false,
                    ShippingStatus = ShippingStatus.Delivering,
                    CreatedOnUtc = DateTime.UtcNow
                };

                _logisticsService.CreatePackageNote(revertNote);
            }
            else
            {
                _logger.Debug("The status is not reverted due to shipment or order is missing. Package# " + package.Id.ToString());
            }
        }

        public virtual bool CanScheduleDelivery(LogisticsPackage package, out bool isFree)
        {
            var handle = CreatePackageHandle(package);
            isFree = handle.IsScheduleDeliveryFree();
            return handle.CanScheduleDelivery(); 
        }

        public virtual bool ScheduleDelivery(LogisticsPackage package, DateTime date, int? warehouseId = null)
        {
            var handle = CreatePackageHandle(package);
            if (!handle.CanScheduleDelivery())
            {
                DumpPackageState("ScheduleDelivery", handle);
                return false;
            }

            if (warehouseId.HasValue)
            {
                handle.WarehousePickUp(warehouseId.Value);
            }
            else
            {
                handle.ScheduleDelivery(date);
            }
            
            return true;
        }

        #endregion

        #region Plan

        /// <summary>
        /// Update all package status in a plan to in trasit
        /// </summary>
        /// <param name="plan"></param>
        public virtual void SetAllPackageDeliveringInPlan(LogisticsPlan plan)
        {
            if (plan == null)
                throw new ArgumentNullException("plan");

            if (!plan.MarkedDelivering)
            {
                plan.AssignedLocations
                    .Where(l => l.Package.Status == ShippingStatus.InWarehouseFailedDelivery || l.Package.Status == ShippingStatus.InWarehouse)
                    .Select(l => l.Package)
                    .ToList()
                    .ForEach(p => UpdateStatus(p, ShippingStatus.Delivering));

                plan.MarkedDelivering = true;
                _logisticsService.UpdateLogisticsPlan(plan, _workContext.CurrentCustomer.Id);
            }
        }

        #endregion

        #endregion
    }
}
